Skip to content

fix: preserve ';', '?', '#' in object keys during resolve()#86

Open
TomNicholas wants to merge 1 commit into
developmentseed:mainfrom
TomNicholas:fix/preserve-special-chars-in-key
Open

fix: preserve ';', '?', '#' in object keys during resolve()#86
TomNicholas wants to merge 1 commit into
developmentseed:mainfrom
TomNicholas:fix/preserve-special-chars-in-key

Conversation

@TomNicholas

Copy link
Copy Markdown

Fixes #85.

Problem

ObjectStoreRegistry.resolve() derived the object key via urlparse(url).path, which silently drops URL params (;), query (?), and fragment (#). So any URL containing those characters had its key truncated.

Two real motivations (details in #85):

  1. Object keys with #/?/; — valid in S3/GCS/Azure keys. E.g. the Cell Painting Gallery has files named DC_DCAM#1_CAM3.tif; resolve() returned .../DC_DCAM, causing a spurious FileNotFound.
  2. Presigned URLs / SAS tokens for private buckets — the signature lives in the query string; dropping ?... strips the auth, so every read 403s. Preserving the query lets a reader access a private bucket via self-authenticating URLs with no local credentials.

Change

registry.py: reattach params/query/fragment to reconstruct the full key.

path = parsed.path
if parsed.params:
    path = f"{path};{parsed.params}"
if parsed.query:
    path = f"{path}?{parsed.query}"
if parsed.fragment:
    path = f"{path}#{parsed.fragment}"

Behavior

Input before after
s3://bucket/path/DC_DCAM#1_CAM3.tif path/DC_DCAM path/DC_DCAM#1_CAM3.tif
s3://bucket/path/obj?v=1.tif path/obj path/obj?v=1.tif
https://host/file.nc?token=abc file.nc (token lost) file.nc?token=abc
https://host/file.nc file.nc file.nc (unchanged)

For HTTP-backed stores this is strictly better: query strings (presigned URLs, SAS tokens) were previously discarded and are now preserved.

Test

Adds a parametrized regression test covering #, ?, and ; in keys. Passes with the fix, fails without it. pytest tests/test_registry.py -k "not async" is green (the 3 async-context-manager tests fail on main too, unrelated to this change). Pre-commit (ruff/mypy/codespell) passes.

Note for reviewers

If a presigned URL resolves to an already-credentialed object store (rather than a generic HTTP store), the reattached query becomes part of the key — a contradictory configuration, and out of scope for both motivations. Happy to gate the reconstruction on store type if you'd prefer.

ObjectStoreRegistry.resolve() derived the object key via urlparse(url).path,
which silently drops anything urlparse treats as URL params (';'), query ('?')
or fragment ('#'). These characters are valid in S3/GCS/Azure object keys
(e.g. 'DC_DCAM#1_CAM3.tif' in the Cell Painting Gallery), so the key was
truncated and lookups failed with a spurious FileNotFound.

Reattach the params/query/fragment to reconstruct the full key. For HTTP-backed
stores this also preserves query strings (presigned URLs, Azure SAS tokens),
which were previously dropped.

@maxrjones maxrjones left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work on this @TomNicholas. I don't think this is the right fix though, for a few reasons:

  1. It doesn't fully solve motivation 1 from #85, since a key ending in a bare ;, ? or # parses to an empty component, so the if parsed.query guard skips it and the character is still lost.
  2. It breaks prefix matching when a query lands at a registration boundary (e.g., https://example.com/data?token=xwill match on the segment data?token=x causing a regression.
  3. It widens the credential leakage surface since SAS tokens and presigned signatures now ride along with the path into tracing and caching wrappers.

I think the right fix is scheme-based dispatch. The object store schemas should have the suffix after the netloc as an opaque key rather than using urlparse at all.

I'd also split out motivation 2 from #85. I think it should be tackled as part of the AiohttpStore overall in #65, since providing a way to route auth is distinct from path handling.

Are you interested in splitting up this PR and adjusting the solution to motivation 1 as requested above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resolve() truncates object keys containing '#', '?', or ';' (breaks special-char keys and presigned URLs)

3 participants